""" svtio holds all the core I/O functions required for
Torchlight save file reading and writing. It is a port of
the Java class, TLSave. """

import os
import sys
import struct
from svtutil import indexof, odict

ENCODING = 'UTF-16LE'
DIFFICULTIES = ['Easy', 'Normal', 'Hard', 'Very Hard']
CHEATER_TRUE = 0xD6
CHEATER_FALSE = 0x4E
PLAYER_DEAD = 0
PLAYER_ALIVE = 17224
BYTE_TRUE = 0x01
BYTE_FALSE = 0x00
GOLD_FF_COUNT = 12

CLASSES = ['Alchemist', 'Destroyer', 'Vanquisher']

PET_TYPES = odict()
PET_TYPES[0] = 'Unknown'
PET_TYPES[-3195029593784249890] = 'Dog'
PET_TYPES[6655120081984033246] = 'Cat'
PET_TYPES[1343088339509383646] = 'Imaginary'
PET_TYPES[6085265292355965406] = 'Ferret'
PET_TYPES[1701168610042778078] = 'Burrower'
PET_TYPES[-550131638619598370] = 'Electric Elemental'
PET_TYPES[-7605033305437564450] = 'Fire Elemental'
PET_TYPES[-6814372775916924450] = 'Ice Elemental'
PET_TYPES[-6143069387552124450] = 'Poison Elemental'
PET_TYPES[-4598024974468640290] = 'Aloe Gel'
PET_TYPES[5391124521138721246] = 'Gel'
PET_TYPES[7931060729150640606] = 'Goblinhound'
PET_TYPES[-5450171995450830370] = 'Spider'
PET_TYPES[2689284772225815006] = 'Troll'
PET_TYPES[-4167721604410830370] = 'Varkolyn'

#some data type constants...
DATA_STRING = 'str'
DATA_CLASS = 'class'
DATA_DIFFICULTY = 'difficulty'
DATA_SHORT = 'short'
DATA_INT = 'int'
DATA_LONG = 'long'
DATA_PET_TYPE = 'pet_type'
DATA_BOOLEAN = 'bool'
DATA_BYTE = 'byte'

SECTION_PLAYER = 'Player'
SECTION_STATS = 'Stats'
SECTION_INVENTORY = 'Inventory'
SECTION_PET = 'Pet'
CATEGORIES = [SECTION_PLAYER, SECTION_STATS, 
			  SECTION_INVENTORY, SECTION_PET]

MAX_VALUES = {
	DATA_SHORT:65535,
	DATA_BYTE:255,
	DATA_INT:4294967295,
	DATA_LONG:sys.maxint
}

def loadCustomBuiltins(path, petDict=None):
	""" Loads custom PET_TYPES and CLASSES """
	global PET_TYPES, CLASSES
	file = open(path, 'r')
	
	reading = None
	pets = petDict if petDict is not None else {}
	classes = []
		
	STR_PETS = '[PET_TYPES]'
	STR_CLASSES = '[CLASSES]'
	
	for line in file:
		line = line.strip()
		if len(line)==0 or line[0]=='#' or line[0]==';':
			pass
		elif line == STR_PETS or line == STR_CLASSES:
			reading = line
		elif reading == STR_PETS:
			k, v = line.split(':')
			pets[int(k)] = v
		elif reading == STR_CLASSES:
			classes.append(line)
	file.close()
	if len(pets)!=0:
		if not pets.has_key(0):
			pets[0] = 'Unknown'
		PET_TYPES = pets
	if len(classes)!=0:
		CLASSES = classes

class TLSave(object):
	file = None
	path = None
	mode = None
	_writing = False
	
	def __init__(self, path, mode):
		self.path = path
		self.mode = mode
		self.reopen()
	
	def reopen(self):
		if self.file is not None:
			self.file.close()
		self.file = open(self.path, self.mode)
		
	def _seekcur(self):
		self.seek(0, os.SEEK_CUR)
	
	def seek(self, pos, whence=os.SEEK_SET):
		self.file.seek(pos, whence)
	
	def step(self, amt):
		if amt!=0:
			self.file.seek(amt, os.SEEK_CUR)

	def flush(self):
		self.file.flush()
			
	def read(self, size):
		if self._writing:
			self._seekcur()
		self._writing = False
		return self.file.read(size)
		
	def readByte(self):
		""" returns an int representing the next byte, or None if EOF """
		s = self.read(1)
		if len(s)==0:
			return None
		return ord(s)
		
	def write(self, str):
		if not self._writing:
			self._seekcur()
		self._writing = True
		self.file.write(str)
	
	def writeByte(self, b):
		self.write(chr(b))
	
	def _unpack(self, size, fmt, f=None):
		if f is None:
			f = self
		s = f.read(size)
		return struct.unpack(fmt, s)[0]
	
	def readShort(self):
		return self._unpack(2, '<h')
	
	def readInt(self):
		return self._unpack(4, '<i')
	
	def readLong(self):
		return self._unpack(8, '<q')
		
	def _pack(self, fmt, s, f=None):
		ar = struct.pack(fmt, s)
		for b in ar:
			if f is None:
				f = self
			f.write(b)
		
	def writeShort(self, s):
		self._pack('<h', s)
	
	def writeInt(self, i):
		self._pack('<i', i)
	
	def writeLong(self, l):
		self._pack('<q', l)
	
	def readString(self):
		""" Reads a 'TL string' as a UTF-8 string """
		s = self.readShort()
		str = self.read(s * 2)
		return str.decode(ENCODING).encode('utf-8')
	
	def writeString(self, str):
		""" Writes the given UTF-8 string as a 'TL string', 
		starting with a 'strlen' short """
		l = len(str)
		self.writeShort(l)
		self.write(str.encode(ENCODING))
		
	def close(self):
		self.file.close()

	def tell(self):
		return self.file.tell()

	def quickFind(self, bArray):
		if bArray is None or len(bArray)==0:
			raise Exception("empty array")
		start = -1
		bInd = 0
		b = ' '
		while len(b)!=0:
			b = self.read(1)
			#we found a correct byte!
			if b == bArray[bInd]:
				if bInd==0: #set starting byte
					start = self.tell()-1
				elif bInd==len(bArray)-1: #we've reached the end
					return start
				bInd+=1
			#did not find the right byte, so reset the index
			elif bInd!=0:
				bInd = 0
		return -1
	
	def nextByteGroup(self, b, x=1, exact=False, seekBack=True): 
		if x==0:
			raise Exception("x can't be 0")
		start = self.tell()
		count = 0
		groupStart = 0
		skipGroup = False
		
		#get next byte
		next = '0'
		while next != '' and next != None:
			next = self.readByte()
			#is that byte what we are looking for?
			#if we are skipping a group, ignore
			if next == b and not skipGroup:
				#it's the start of a group
				if count==0:
					groupStart = self.tell()-1
				count+=1
				#if we have found exactly X of them
				if count == x:
					#don't search for exactly X, so it doesn't matter what's next
					#just move & return
					if not exact:
						if seekBack:
							self.seek(groupStart)
						return True
                    
					lookahead = self.readByte()
					#otherwise.. if it is exact
					#and it's not the EOF, let's lookahead
					if lookahead != '' and lookahead != None:
						#yup, we had found the end of a group
						#reset the pointer to the start of the group
						if lookahead != b:
							if seekBack:
								self.seek(groupStart)
							else:
								self.step(-1)
							return True
						else:
							#it's a group, but not the right size.. ignore it
							skipGroup = True
							self.step(-1)
							count = 0
					#we've reached the EOF with exactly X.. it's a group
					else:
						if seekBack:
							self.seek(groupStart)
						return True
			elif next != b:
				#it's not the byte we are looking for... reset everything
				skipGroup = False
				count = 0
				groupStart = 0
		if seekBack:
			self.seek(start)
		return False
	
	def replaceString(self, newStr, reopen=True):
		"""
		Replaces a string, adding new bytes if necessary. 
		Only supported for player saves (e.g. not for shared stash, yet). 
		The file will be reopened if the insert operation was successful.
		(And seeked to the end of the new string)
		
		Behaves like writeString if the new string is of the same size.
		"""
		start = self.tell()
		oldStrlen = self.readShort()
		newStrlen = len(newStr)
		
		self.seek(-4, os.SEEK_END)
		oldLength = self.readInt()
		newLength = oldLength + (newStrlen - oldStrlen) * 2
		
		if newLength==oldLength:
			self.seek(start)
			self.writeString(newStr)
		else:
			#update the file length before we start writing data
			self.seek(-4, os.SEEK_END)
			self.writeInt(newLength)
			
			#now seek to the start of the file
			self.seek(0)
			
			#copy the first part to a temp file
			tmppath = self.path+'~'
			tmp = open(tmppath, 'wb')
			bufsize = 1024
			startLen = start
			readLen = min(bufsize, startLen)
			while 1:
				buf = self.read(readLen)
				if buf:
					tmp.write(buf)
					startLen -= len(buf)
					readLen = min(bufsize, startLen)
				else:
					break
			
			#write the new string to the temp file
			self._pack('<h', newStrlen, f=tmp)
			tmp.write(newStr.encode(ENCODING))
			
			#in the save file, step ahead past the old name
			self.step(2 + oldStrlen * 2)
			
			#now copy the rest of the file
			while 1:
				buf = self.read(bufsize)
				if buf:
					tmp.write(buf)
				else:
					break
			
			#close both files
			self.close()
			tmp.close()
			
			#delete the original, rename the temp...
			os.remove(self.path)
			os.rename(tmppath, self.path)
			if reopen:
				self.reopen()
				self.seek(start + 2 + newStrlen * 2)

class TLPlayerSave(TLSave):
	
	readAll = False
	props = None
	DO_NOTHING = -1
	petNamePtr = 0
	
	propCount = 0
	nogold = False
	nopet = False
	
	details = None
	
	def __init__(self, path, mode):
		TLSave.__init__(self, path, mode)
	
	def keyforvalue(self, dict, value):
		for k, v in dict.items():
			if v == value:
				return k
		return None
		
	def values2text(self, props):
		""" None values will not be changed """
		for k, v in props.items():
			if v is not None:
				if k=='difficulty':
					props[k] = DIFFICULTIES[v]
				elif k=='pet_type':
					typ = PET_TYPES.get(v)
					props[k] = typ if typ is not None else 'Unknown'
				elif k=='cheater':
					props[k] = 'true' if v==CHEATER_TRUE else 'false'
				elif k=='dead':
					props[k] = 'true' if v==PLAYER_DEAD else 'false'
				elif k in ['hardcore', 'retired']:
					props[k] = 'true' if v==BYTE_TRUE else 'false'
    
	def _bool(self, v):
		return str(v).lower() == 'true'
	def _fbool(self, v):
		return str(v).lower() == 'false'
	
	def text2values(self, props):
		""" None values will not be changed """
		for k, v in props.items():
			if v is not None:
				if k=='difficulty':
					props[k] = indexof(DIFFICULTIES, v)
				elif k=='pet_type':
					props[k] = self.keyforvalue(PET_TYPES, v) or 0
				elif k=='cheater':
					props[k] = CHEATER_TRUE if self._bool(v) else CHEATER_FALSE
				elif k=='dead':
					props[k] = PLAYER_DEAD if self._bool(v) else PLAYER_ALIVE
				elif self._bool(v):
					props[k] = BYTE_TRUE
				elif self._fbool(v):
					props[k] = BYTE_FALSE
	
	def update(self, props={}, readAll=False, impl=None):
		"""
		If properties is None or empty, or if readAll is True, 
		all properties will be read into the properties dict.
		Any values that are None will be read. Any values that
		are not None will be edited.
		If the dict includes a non-None pet name and/or player
		name, then the file may need to be opened and closed multiple
		times to change its size.
		
		If impl is not None, it will allow you to pass a custom seek method.
		If passed, readAll will be considered True.
		Example:
			def foo(self, save):
				save.next(4, 'class', save.STRING)
				save.next(81, 'name', save.STRING)
				save.next(84, 'level', save.INT)
			
			save.update(impl=foo)
		
		All properties are expected in "value" form 
		(i.e. CHEATER_TRUE instead of "true"). Use the conversion methods
		to convert back and forth.
		
		Values that (currently) cannot be changed:
		  - class name
		  - player name
		  - pet name
		"""
		self.details = []
		self.seek(0)
		
		if impl is not None:
			readAll = True
		self.readAll = readAll or props is None or len(props) == 0
		self.props = props or {}
		self.propCount = len(self.props)
		
		#start reading/writing properties...
		if impl is not None:
			impl(self)
		else:
			self.next(4, 'class', self.STRING, DATA_CLASS)
			self.next(0, 'difficulty', self.BYTE, DATA_DIFFICULTY)
			self.next(3, 'hardcore', self.BYTE, DATA_BOOLEAN)
			self.next(4, 'retired', self.BYTE, DATA_BOOLEAN)
			self.next(30, 'cheater', self.BYTE, DATA_BOOLEAN)
			self.next(40, 'name', self.STRING, DATA_STRING)
			self.next(84, 'level', self.INT, DATA_INT, SECTION_STATS)
			self.next(0, 'levelxp', self.INT, DATA_INT, SECTION_STATS, 'Level XP')
			self.next(0, 'fame', self.INT, DATA_INT, SECTION_STATS, 'Fame Level')
			self.next(0, 'famexp', self.INT, DATA_INT, SECTION_STATS, 'Fame XP')
			self.next(4, 'health', self.INT, DATA_INT, SECTION_STATS, 'Health Points')
			self.next(0, 'hpbonus', self.INT, DATA_INT, SECTION_STATS, 'HP Bonus')
			self.next(2, 'dead', self.SHORT, DATA_BOOLEAN, name='Dead')
			self.next(0, 'mana', self.INT, DATA_INT, SECTION_STATS, 'Mana Points')
			self.next(20, 'statpoints', self.INT, DATA_INT, SECTION_STATS, 'Stat Points')
			self.next(0, 'skillpoints', self.INT, DATA_INT, SECTION_STATS, 'Skill Points')
			if not self.nogold:
				self.next(0, 'gold', self.GOLD, DATA_INT, SECTION_INVENTORY)
			if not self.nopet:
				self.next(0, 'pet_name', self.PET_NAME, DATA_STRING, SECTION_PET, 'Pet Name')
				self.next(0, 'pet_type', self.PET_TYPE, DATA_PET_TYPE, SECTION_PET, 'Pet Type')
	
	def _func(self, val, readFunc, writeFunc, skipAmt=None):
		if skipAmt is not None:
			self.step(skipAmt)
			return None
		if val is None:
			return readFunc()
		else:
			writeFunc(val)
			return val
	
	def STRING(self, val, skipping=False):
		val = str(val) if val is not None else val
		if skipping:
			self.step( self.readShort() * 2 )
			return None
		else:
			return self._func(val, self.readString, self.replaceString)
	
	def GOLD(self, val, skipping=False):
		val = int(val) if val is not None else val
		before = self.tell()
		success = self.nextByteGroup(0xFF, GOLD_FF_COUNT, True)
		if success and not skipping:
			self.step(-8)
			if val is None:
				return self.readInt()
			else:
				self.writeInt(val)
				return val
		elif not success:
			self.seek(before)
		return -1
	
	def PET_TYPE(self, val, skipping=False):
		#we assume that the pet name was just read...
		val = int(val) if val is not None else val
		if self.petNamePtr<=0:
			return 0
		self.seek(self.petNamePtr-70)
		if val==0:
			skipping = True
		return self.LONG(val, skipping)
	
	def PET_NAME(self, val, skipping=False):
		#make sure the file pointer is well after the class name
		val = str(val) if val is not None else val
		before = self.tell()
		start = 450
		if self.tell() < start:
			self.seek(start)
		
		#search for the pet hash
		self.quickFind('INNATEARMOR'.encode(ENCODING));
		
		if self.nextByteGroup(0xFF, 12, True, False):
			self.petNamePtr = self.tell()
			return self.STRING(val, skipping)
		self.seek(before)
		return 'Unknown'
	
	def BYTE(self, val, skipping=False):
		val = int(val) if val is not None else val
		skip = 1 if skipping else None
		return self._func(val, self.readByte, self.writeByte, skip)
	
	def SHORT(self, val, skipping=False):
		val = int(val) if val is not None else val
		skip = 2 if skipping else None
		return self._func(val, self.readShort, self.writeShort, skip)
	
	def INT(self, val, skipping=False):
		val = int(val) if val is not None else val
		skip = 4 if skipping else None
		return self._func(val, self.readInt, self.writeInt, skip)
	
	def LONG(self, val, skipping=False):
		val = long(val) if val is not None else val
		skip = 8 if skipping else None
		return self._func(val, self.readLong, self.writeLong, skip)
	
	def namefix(self, name):
		name.replace('_', ' ')
		if len(name)<=1:
			return name
		else:
			return name[0].upper() + name[1:]
	
	def next(self, stepAmt, key, func, dataType=None, 
			 category=SECTION_PLAYER, name=None, noEdit=False):
		""" will return immediately if all props are "popped" """
		if not self.readAll and self.propCount <= 0:
			return
		
		self.step(stepAmt)
		v = self.props.get(key)
		ignore = key not in self.props and not self.readAll
		
		#if property does not exist, only read/write if readAll is True
		if not ignore:
			if noEdit:
				v = None
			v = func(v)
			self.props[key] = v
			self.propCount -= 1
			#add some details, which is useful for GUI apps
			if name is None:
				name = self.namefix(key)				
			self.details.append({
				'key':key, 'category':category, 'name':name,
				'data':dataType, 'noEdit':noEdit})
		#if we aren't reading/writing, we must skip ahead of this...
		else:
			func(None, skipping=True)